/* Alloy Analyzer 4 -- Copyright (c) 2006-2009, Felix Chang
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files
 * (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify,
 * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
 * OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

package edu.mit.csail.sdg.alloy4;

import java.io.IOException;
import java.io.File;
import java.io.RandomAccessFile;
import java.awt.image.BufferedImage;
import javax.imageio.ImageIO;

/** Graphical convenience methods for producing PNG files. */

public final strictfp class OurPNGWriter {

   /** The constructor is private, since this utility class never needs to be instantiated. */
   private OurPNGWriter () { }

   /** Writes the image as a PNG file with the given horizontal and vertical dots-per-inch. */
   public static void writePNG (BufferedImage image, String filename, double dpiX, double dpiY) throws IOException {
      try {
         ImageIO.write(image, "PNG", new File(filename)); // some versions of Java sometimes throws an exception during saving...
         setDPI(filename, dpiX, dpiY);
      } catch(Throwable ex) {
         if (ex instanceof IOException) throw (IOException)ex;
         if (ex instanceof StackOverflowError) throw new IOException("Out of memory trying to save the PNG file to " + filename);
         if (ex instanceof OutOfMemoryError) throw new IOException("Out of memory trying to save the PNG file to " + filename);
         throw new IOException("Error writing the PNG file to " + filename + " (" + ex + ")");
      }
   }

   /* PNG consists of a "8 byte header" followed by one or more CHUNK...
    *
    * Each CHUNK:
    * ===========
    * 4 bytes: an integer N expressed with most-significant-byte first
    * 4 bytes: Chunk Type
    * N bytes: Chunk Data
    * 4 bytes: Checksum (this checksum is computed over the Chunk Type and Chunk Data)
    *
    * Each PNG must contain an IDAT chunk (this is the actual pixels of the image)
    *
    * Each PNG may contain an optional pHYs chunk that describes the horizontal and vertical dots-per-meter information.
    * If such a chunk exists, it must come before (though not necessarily immediately before) the IDAT chunk.
    *
    * pHYs CHUNK:
    * ===========
    * 4 bytes:  0  ,  0  ,  0  ,  9
    * 4 bytes: 'p' , 'H' , 'Y' , 's'
    * 4 bytes: horizontal dots per meter  (most-significant-byte first)
    * 4 bytes: vertical dots per meter    (most-significant-byte first)
    * 1 byte:   1
    * 4 bytes: Checksum
    */

   /** Modifies the given PNG file to have the given horizontal and vertical dots-per-inch. */
   private static void setDPI (String filename, double dpiX, double dpiY) throws IOException {
      RandomAccessFile f = null;
      try {
         f = new RandomAccessFile(filename, "rw");
         for(long total=f.length(), pos=8; pos<total;) { // Jump to the right place for inserting the pHYs chunk, then insert it
            f.seek(pos);
            int a1 = f.read(); if (a1 < 0) break;
            int a2 = f.read(); if (a2 < 0) break;
            int a3 = f.read(); if (a3 < 0) break;
            int a4 = f.read(); if (a4 < 0) break;
            int b1 = f.read(); if (b1 < 0) break;
            int b2 = f.read(); if (b2 < 0) break;
            int b3 = f.read(); if (b3 < 0) break;
            int b4 = f.read(); if (b4 < 0) break;
            int jump = (a1<<24) | (a2<<16) | (a3<<8) | a4;
            if (jump<0 || (total-pos)-12<jump) throw new IOException("PNG chunk size exceeds the rest of the file.");
            if ((b1=='I' && b2=='D' && b3=='A' && b4=='T') || (b1=='p' && b2=='H' && b3=='Y' && b4=='s')) {
               Util.shift(f, (b1=='p' ? (pos+12+jump) : pos), pos+21); // skip over the existing pHYs chunk if we see it
               f.seek(pos);
               writeDPI(f, dpiX, dpiY);
               f.close();
               f = null;
               return; // use a "return" rather than "break" so that we don't enter the line that throws the IOException
            }
            pos = pos + 12 + jump;
         }
         throw new IOException("PNG is missing the IDAT chunk");
      } finally {
         Util.close(f);
      }
   }

   /** Write a "pHYs" chunk into the given file with the given horizontal and vertical dots-per-inch. */
   private static void writeDPI (RandomAccessFile file, double dpiX, double dpiY) throws IOException {
      int x = (int) (dpiX/0.0254), y = (int) (dpiY/0.0254); // Translate dots-per-inch into dots-per-meter
      writeChunk(file, new int[] {'p', 'H', 'Y', 's', x>>>24, x>>>16, x>>>8, x, y>>>24, y>>>16, y>>>8, y, 1});
   }

   /** Write the given chunk into the given file;  Note: data.length must be at least 4. */
   private static void writeChunk (RandomAccessFile file, int[] data) throws IOException {
      int crc = (-1), len = data.length - 4;
      file.write((len>>>24) & 255); file.write((len>>>16) & 255); file.write((len>>>8) & 255); file.write(len & 255);
      for(int i=0; i<data.length; i++) { int x = data[i]; crc = table[(crc ^ x) & 255] ^ (crc >>> 8); file.write(x & 255); }
      crc = crc ^ (-1);
      file.write((crc>>>24) & 255); file.write((crc>>>16) & 255); file.write((crc>>>8) & 255); file.write(crc & 255);
   }

   /** This precomputed table makes it faster to calculate CRC; this is based on the suggestion in the PNG specification. */
   private static final int[] table = new int[] {
      0,1996959894,-301047508,-1727442502,124634137,1886057615,-379345611,-1637575261,249268274
      ,2044508324,-522852066,-1747789432,162941995,2125561021,-407360249,-1866523247,498536548,1789927666
      ,-205950648,-2067906082,450548861,1843258603,-187386543,-2083289657,325883990,1684777152,-43845254
      ,-1973040660,335633487,1661365465,-99664541,-1928851979,997073096,1281953886,-715111964,-1570279054
      ,1006888145,1258607687,-770865667,-1526024853,901097722,1119000684,-608450090,-1396901568,853044451
      ,1172266101,-589951537,-1412350631,651767980,1373503546,-925412992,-1076862698,565507253,1454621731
      ,-809855591,-1195530993,671266974,1594198024,-972236366,-1324619484,795835527,1483230225,-1050600021
      ,-1234817731,1994146192,31158534,-1731059524,-271249366,1907459465,112637215,-1614814043,-390540237
      ,2013776290,251722036,-1777751922,-519137256,2137656763,141376813,-1855689577,-429695999,1802195444
      ,476864866,-2056965928,-228458418,1812370925,453092731,-2113342271,-183516073,1706088902,314042704
      ,-1950435094,-54949764,1658658271,366619977,-1932296973,-69972891,1303535960,984961486,-1547960204
      ,-725929758,1256170817,1037604311,-1529756563,-740887301,1131014506,879679996,-1385723834,-631195440
      ,1141124467,855842277,-1442165665,-586318647,1342533948,654459306,-1106571248,-921952122,1466479909
      ,544179635,-1184443383,-832445281,1591671054,702138776,-1328506846,-942167884,1504918807,783551873
      ,-1212326853,-1061524307,-306674912,-1698712650,62317068,1957810842,-355121351,-1647151185,81470997
      ,1943803523,-480048366,-1805370492,225274430,2053790376,-468791541,-1828061283,167816743,2097651377
      ,-267414716,-2029476910,503444072,1762050814,-144550051,-2140837941,426522225,1852507879,-19653770
      ,-1982649376,282753626,1742555852,-105259153,-1900089351,397917763,1622183637,-690576408,-1580100738
      ,953729732,1340076626,-776247311,-1497606297,1068828381,1219638859,-670225446,-1358292148,906185462
      ,1090812512,-547295293,-1469587627,829329135,1181335161,-882789492,-1134132454,628085408,1382605366
      ,-871598187,-1156888829,570562233,1426400815,-977650754,-1296233688,733239954,1555261956,-1026031705
      ,-1244606671,752459403,1541320221,-1687895376,-328994266,1969922972,40735498,-1677130071,-351390145
      ,1913087877,83908371,-1782625662,-491226604,2075208622,213261112,-1831694693,-438977011,2094854071
      ,198958881,-2032938284,-237706686,1759359992,534414190,-2118248755,-155638181,1873836001,414664567
      ,-2012718362,-15766928,1711684554,285281116,-1889165569,-127750551,1634467795,376229701,-1609899400
      ,-686959890,1308918612,956543938,-1486412191,-799009033,1231636301,1047427035,-1362007478,-640263460
      ,1088359270,936918000,-1447252397,-558129467,1202900863,817233897,-1111625188,-893730166,1404277552
      ,615818150,-1160759803,-841546093,1423857449,601450431,-1285129682,-1000256840,1567103746,711928724
      ,-1274298825,-1022587231,1510334235,755167117
   };
}
